﻿''' <summary>
''' Provides a character and color buffer for screen updates to the console window.
''' </summary>
Public Class ConsoleScreenBuffer

    Private character() As Char
    Private forecolor() As ConsoleColor
    Private backcolor() As ConsoleColor
    Private dirty() As Boolean

    Private characterState() As Char
    Private forecolorState() As ConsoleColor
    Private backcolorState() As ConsoleColor
    Private isInvalidating As Boolean

    ''' <summary>
    ''' Gets or sets the background color at the specified location.
    ''' </summary>
    ''' <param name="x"></param>
    ''' <param name="y"></param>
    ''' <returns></returns>
    Public Property BackgroundColor(x As Integer, y As Integer) As ConsoleColor
        Get
            Return GetBackcolor(x, y)
        End Get
        Set(value As ConsoleColor)
            SetBackcolor(x, y, value)
        End Set
    End Property

    Private _bounds As Rectangle
    ''' <summary>
    ''' Gets the bounds of this screen area represented by this buffer
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property Bounds As Rectangle
        Get
            Return _bounds
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets the forground color at the specified location
    ''' </summary>
    ''' <param name="x"></param>
    ''' <param name="y"></param>
    ''' <returns></returns>
    Public Property ForegroundColor(x As Integer, y As Integer) As ConsoleColor
        Get
            Return GetForecolor(x, y)
        End Get
        Set(value As ConsoleColor)
            SetForecolor(x, y, value)
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the character at the specified location.
    ''' </summary>
    ''' <param name="x"></param>
    ''' <param name="y"></param>
    ''' <returns></returns>
    Default Public Property Item(x As Integer, y As Integer) As Char
        Get
            Return GetBufferChar(x, y)
        End Get
        Set(value As Char)
            SetBufferChar(x, y, value)
        End Set
    End Property

    ''' <summary>
    ''' Saves the internal state of the buffer prior to updates being performed.  EndInvalidate must be called
    ''' to compare changes in the buffer state prior to calling Refresh on the buffer.
    ''' </summary>
    Public Sub BeginInvalidate()
        If Not isInvalidating Then
            isInvalidating = True
            character.CopyTo(characterState, 0)
            forecolor.CopyTo(forecolorState, 0)
            backcolor.CopyTo(backcolorState, 0)
        End If
    End Sub

    ''' <summary>
    ''' Compares the current state of the buffer to the state saved prior to updates being performed and
    ''' dirties all positions with changes.
    ''' </summary>
    Public Sub EndInvalidate()
        If isInvalidating Then
            For i As Integer = 0 To characterState.Length - 1
                Dim didchange As Boolean = False
                didchange = Not (characterState(i).Equals(character(i)))
                If Not didchange Then didchange = Not (forecolorState(i).Equals(forecolor(i)))
                If Not didchange Then didchange = Not (backcolorState(i).Equals(backcolor(i)))
                dirty(i) = didchange
            Next
            isInvalidating = False
        End If
    End Sub

    ''' <summary>
    ''' Sets the specified position to a null character which is not drawn when the buffer is rendered.
    ''' </summary>
    ''' <param name="x"></param>
    ''' <param name="y"></param>
    Public Sub Clear(x As Integer, y As Integer)
        SetBufferChar(x, y, Chr(0))
    End Sub

    ''' <summary>
    ''' Sets all positions to null characters which are not drawn when the buffer is rendered.
    ''' </summary>
    Public Sub ClearBuffer()
        Dim nullC = Chr(0)
        For y = 0 To _bounds.Height - 1
            For x = 0 To _bounds.Width - 1
                SetBufferChar(x, y, nullC)
            Next
        Next
    End Sub

    ''' <summary>
    ''' Copies the state of this buffer into another buffer.  This buffer's bounds must fit within
    ''' the target buffer's bounds.
    ''' </summary>
    ''' <param name="other"></param>
    Public Sub CopyTo(other As ConsoleScreenBuffer)
        Dim nullC = Chr(0)
        For y = 0 To _bounds.Height - 1
            For x = 0 To Bounds.Width - 1
                Dim c = GetBufferChar(x, y)
                If Not c = nullC Then
                    other.SetBufferChar(x + Bounds.Left, y + Bounds.Top, c)
                    other.SetForecolor(x + Bounds.Left, y + Bounds.Top, GetForecolor(x, y))
                    other.SetBackcolor(x + Bounds.Left, y + Bounds.Top, GetBackcolor(x, y))
                End If

            Next
        Next
    End Sub

    ''' <summary>
    ''' Redraws the buffer to the console window.
    ''' </summary>
    Public Sub Refresh()
        If isInvalidating Then Throw New InvalidOperationException("Attempt to call refresh while buffer is invalidated.")
        Using New ConsoleStateSave
            Dim nullC = Chr(0)
            Dim xMax = Console.BufferWidth - 1 - (Console.BufferWidth - Console.WindowWidth)
            Dim yMax = Console.BufferHeight - (Console.BufferHeight - Console.WindowHeight) - 1
            For y As Integer = _bounds.Top To _bounds.Bottom
                If y > yMax Then Exit For
                For x As Integer = _bounds.Left To _bounds.Right
                    If x > xMax Then Exit For
                    Dim index = y * _bounds.Width + x
                    If index > -1 AndAlso index < character.Length AndAlso dirty(index) = True Then
                        Dim c = character(index)
                        If Not c = nullC Then
                            Console.BackgroundColor = backcolor(index)
                            Console.ForegroundColor = forecolor(index)
                            Console.SetCursorPosition(x, y)
                            Console.Write(c)
                        End If
                        dirty(index) = False
                    End If
                Next
            Next
        End Using
    End Sub

    ''' <summary>
    ''' Updates a horizontal series of characters starting at the specified position.
    ''' </summary>
    ''' <param name="x">The left-most starting position of the string represented by value.</param>
    ''' <param name="y">The top position of the string.</param>
    ''' <param name="value">The string of characters to write to the buffer.</param>
    Public Sub SetLine(x As Integer, y As Integer, value As String)
        If Not String.IsNullOrEmpty(value) Then
            For Each c In value
                SetBufferChar(x, y, c)
                x += 1
            Next
        End If
    End Sub

    Protected Function GetBackcolor(x As Integer, y As Integer) As ConsoleColor
        Dim index = y * _bounds.Width + x
        If index > -1 AndAlso index < character.Length Then Return backcolor(index)
        Return Console.BackgroundColor
    End Function

    Protected Function GetBufferChar(x As Integer, y As Integer) As Char
        Dim index = y * _bounds.Width + x
        If index > -1 AndAlso index < character.Length Then Return character(index)
        Return Chr(0)
    End Function

    Protected Function GetForecolor(x As Integer, y As Integer) As ConsoleColor
        Dim index = y * _bounds.Width + x
        If index > -1 AndAlso index < character.Length Then Return forecolor(index)
        Return Console.ForegroundColor
    End Function

    Protected Friend Sub Resize(windowBounds As Rectangle)
        _bounds = windowBounds
        ReDim Preserve character(windowBounds.Width * windowBounds.Height - 1)
        ReDim Preserve forecolor(windowBounds.Width * windowBounds.Height - 1)
        ReDim Preserve backcolor(windowBounds.Width * windowBounds.Height - 1)
        ReDim Preserve characterState(windowBounds.Width * windowBounds.Height - 1)
        ReDim Preserve forecolorState(windowBounds.Width * windowBounds.Height - 1)
        ReDim Preserve backcolorState(windowBounds.Width * windowBounds.Height - 1)
        ReDim Preserve dirty(windowBounds.Width * windowBounds.Height - 1)
        For i As Integer = 0 To character.Length - 1
            If character(i) = Chr(0) Then
                character(i) = Chr(32)
                dirty(i) = True
            End If
        Next
    End Sub

    Protected Sub SetBackcolor(x As Integer, y As Integer, value As ConsoleColor)
        Dim index = y * _bounds.Width + x
        If index > -1 AndAlso index < character.Length Then
            backcolor(index) = value
        End If
    End Sub

    Protected Sub SetBufferChar(x As Integer, y As Integer, value As Char)
        Dim index = y * _bounds.Width + x
        If index > -1 AndAlso index < character.Length Then
            character(index) = value
        End If
    End Sub

    Protected Sub SetForecolor(x As Integer, y As Integer, value As ConsoleColor)
        Dim index = y * _bounds.Width + x
        If index > -1 AndAlso index < character.Length Then
            forecolor(index) = value
        End If
    End Sub

End Class